<?php

/*
 * To change this license header, choose License Headers in Project Properties.
 * To change this template file, choose Tools | Templates
 * and open the template in the editor.
 */

namespace Core\Common\Base;

/**
 * Description of Service
 *
 * @author baiyang@ibantang.com
 */
class Service extends Cache
{
    /**
     * 已注册的服务信息
     */
    const SERVICE_NAME = 'core_service';
    const SERVICE_TABLE_SIZE = 4096;

    /**
     * 被调用服务的网关信息
     */
    const SERVICE_GATEWAY_ROUTE = 'core_service_gateway';
    const SERVICE_GATEWAY_TABLE_SIZE = 1;
    private static $gatewayRouteConfig = null;

    /**
     * 服务启动相关table初始化，如果出错，则程序终止执行
     */
    public static function serviceBootstrap()
    {
        $maxTries = 5;
        self::initTable();
        self::initServiceGatewayRouteBootstrap($maxTries);
        self::initDiscoveryServiceBootstrap($maxTries);
    }

    /**
     * 初始化内存cache
     */
    public static function initTable()
    {
        Cache::addTable(self::SERVICE_NAME, [
            'value' => [Cache::TYPE_STRING, 300],
        ], self::SERVICE_TABLE_SIZE);
        Cache::addTable(self::SERVICE_GATEWAY_ROUTE, [
            'value' => [Cache::TYPE_STRING, 409600],
        ], self::SERVICE_GATEWAY_TABLE_SIZE);
        RoundRobin::initRoundRobinTable(self::SERVICE_TABLE_SIZE);
    }

    /**
     * 初始化服务调用路由规则，如果出错，则程序终止执行
     * @var $maxTries int 最大重试次数
     */
    public static function initServiceGatewayRouteBootstrap($maxTries = 5)
    {
        $configRet = ConfigSource::getGatewayRouteConfig($maxTries);
        $value = $configRet["value"] ?? "";
        $error = $configRet["error"] ?? "";
        if (!empty($error)) {
            Log::coreLog($error);
            exit;
        }

        // 验证格式
        $config = json_decode($value, true);
        $isError = false;
        if (empty($config)) {
            $isError = true;
        }
        if (!$isError) {
            if (!isset($config["data"])) {
                $isError = true;
            }
        }
        if ($isError) {
            Log::coreLog([
                "parse gateway route config error, consul path is:",
                ConfigSource::$serviceGatewayRouteKVName,
                "raw data is: ",
                $value,
            ]);
            exit;
        }
        self::$gatewayRouteConfig = $config;
    }

    /**
     * 初始化服务发现信息
     * @param int $maxTries
     */
    private static function initDiscoveryServiceBootstrap($maxTries = 5)
    {
        $times = 0;
        while (++$times < $maxTries) {
            $startTime = microtime(true);
            $ret = self::getHealthServices();
            $now = time();
            $count = 0;
            if ($ret && is_array($ret) && !empty($ret["data"])) {
                $count = count($ret["data"]);
                foreach ($ret["data"] as $k => $v) {
                    self::setService($k, $v, $now);
                }
            }
            $endTime = microtime(true);
            Log::coreLog([
                sprintf("discovery service (%s times):", $times),
                json_encode($ret),
                sprintf("found %s services, use %s seconds", $count, $endTime - $startTime),
            ]);

            // check gateway info
            $gateway = self::getService("sqkb-ares");
            if (!empty($gateway)) {
                // everything is OK
                break;
            } else {
                Log::coreLog([
                    sprintf("get gateway address error (%s times), ", $times),
                    $gateway,
                ]);
            }
        }
        // check again
        $gateway = self::getService("sqkb-ares");
        if (empty($gateway)) {
            Log::coreLog([
                sprintf("get gateway address error (%s times), ", $times),
                $gateway,
            ]);
        }
    }

    /**
     * 从consul发现服务地址信息
     * @return array
     */
    public static function getHealthServices()
    {
        $ret = [];
        $error = null;
        $autoRegisterServiceTag = "k8s";
        $needDiscoveryServices = [
            "grpc",
            "ares",
            "gateway",
        ];
        try {
            $catalog = Consul::getInstance()->Catalog();
            list($data, $qm, $error) = $catalog->services();
            if (!$data) {
                return ["data" => $ret, "error" => $error,];
            }
            $health = Consul::getInstance()->Health();
            foreach ($data as $name => $tags) {
                if ('consul' == $name || CORE_NAME == $name) {
                    continue;
                }

                // k8s和宿主机混合部署时，手动进行健康检查
                $passingOnly = true;
                if (in_array($autoRegisterServiceTag, $tags)) {
                    $passingOnly = false;
                }

                // 过滤掉不需要的服务
                $passed = false;
                foreach ($needDiscoveryServices as $needName) {
                    if (strpos($name, $needName) !== false) {
                        $passed = true;
                        break;
                    }
                }
                if (!$passed) {
                    continue;
                }

                // 获取服务地址信息
                list($serviceInfo, $qm, $err) = $health->service($name, '', $passingOnly);
                if ($err || empty($serviceInfo)) {
                    continue;
                }

                foreach ($serviceInfo as $info) {
                    // 如果tags里包含了k8s，则不需要进行健康检查，否则需要看健康状态
                    if (!empty($info->Service->Tags)) {
                        $tmpTags = $info->Service->Tags ?? [];
                        $needCheckHealth = false;
                        if (!in_array($autoRegisterServiceTag, $tmpTags)) {
                            $needCheckHealth = true;
                        }
                        if ($needCheckHealth) {
                            $tmpChecks = $info->Checks ?? [];
                            $tmpStatus = [];
                            $tmpCount = count($tmpChecks);
                            /**
                             * @var $tmpCheck \DCarbone\PHPConsulAPI\Health\HealthCheck
                             */
                            foreach ($tmpChecks as $tmpCheck) {
                                $tmp = $tmpCheck->getStatus();
                                if ("passing" != $tmp) {
                                    break;
                                }
                                $tmpStatus[] = $tmp;
                            }
                            if (count($tmpStatus) != $tmpCount) {
                                Log::coreLog([
                                    "in serviceHander, reject service Node (health not pass), ",
                                    $name,
                                    $info->Service->ID,
                                    $info->Service->Address,
                                ]);
                                continue;
                            }
                        }
                    }
                    $found = 0;
                    if (!empty($info->Service->Meta)) {
                        foreach ($info->Service->Meta as $k => $v) {
                            if (in_array($k, ['http', 'grpc', 'tcp'])) {
                                $found++;
                                $ret[$name . ':' . $k][] = $info->Service->Address . ':' . $v;
                                $ret[$name . ':' . $k] = array_values(array_unique($ret[$name . ':' . $k]));
                            }
                        }
                    }
                    if ($found <= 0 && !empty($info->Service)) {
                        $protocol = ':http';
                        if (substr($name, -4) == 'grpc') {
                            // 同时兼容 _grpc 和 -grpc
                            $protocol = ':grpc';
                        }
                        $ret[$name . $protocol][] = $info->Service->Address . ':' . $info->Service->Port;
                        $ret[$name . $protocol] = array_values(array_unique($ret[$name . $protocol]));
                    }
                }
            }
        } catch (Exception $e) {
            $error = $e->getMessage();
        }
        return ["data" => $ret, "error" => $error,];
    }

    /**
     * 获取目标服务的地址信息，这里不再实时从consul获取，使用的是cache
     * @param $name
     * @param string $proto
     * @return mixed|null
     */
    public static function getService($name, $proto = 'http')
    {
        $srvId = self::getServiceId($name, $proto);
        $instance = Cache::get(self::SERVICE_NAME);
        if (empty($instance)) {
            return null;
        }

        $val = $instance->get($srvId);
        if (empty($val) || empty($val['value'])) {
            return null;
        }

        return json_decode(gzinflate($val['value']), true);
    }

    /**
     * 保存服务信息到cache，异步进程来调用
     * @param $name
     * @param string $value
     * @param null $expire
     * @return bool
     */
    public static function setService($name, $value = '', $expire = null)
    {
        if (empty($value)) {
            return false;
        }
        $instance = Cache::get(self::SERVICE_NAME);
        if (is_string($value)) {
            $instance->set($name, ['value' => gzdeflate($value), 'expire' => $expire]);
        } else {
            $instance->set($name, ['value' => gzdeflate(json_encode($value)), 'expire' => $expire,]);
        }
        return true;
    }

    /**
     * 返回服务唯一id
     * @param $service
     * @param string $proto
     * @return string
     */
    public static function getServiceId($service, $proto = 'http')
    {
        return $service . ':' . $proto;
    }

    /**
     * 获取网关路由配置信息
     * @return array
     */
    public static function getServiceGatewayRouteInfo()
    {
        return self::$gatewayRouteConfig;
    }
}
